#!/usr/bin/perl

use strict;
use warnings;

use FindBin qw($Bin $Script);
use Cwd qw(getcwd abs_path);
$Bin = abs_path($Bin);

use Digest::SHA qw(sha256_hex);
use Storable qw(nstore retrieve store_fd nstore_fd freeze thaw dclone);

# The user administration tool can perform 3 main functions:
# 1) It can authenticate a user using the username and password, 2) It can retreive a users tye (A, C, or M), 
# 3) It can perform user admin functions.  Only Administrators have full control of the admin functions, but 
# Control and Monitor users may login and change their own password.
#
# Functions 1) and 2) work by the use of exit codes to return results to other modules that may want to authenticate to the user database.  
# Exit Codes:
# 0: user admin tool exited succesfully,
# 1: initial arguments are bad, 2: user was not found, 3: user is disabled,
# 4: user is an administrator, 5: user is a control user, 6: user is a monitor user, 7: password supplied is not valid
# 8: user is an administrator with a valid password, 9: user is a control user with a valid password, 10: user is a monitor user with a valid password
# 11: user is a guest, 12: user a guest with a valid password, 13: user is an expired guest.  account is now deleted.

#  This is CLI tool, thus we require arguments.
my $numArgs = $#ARGV + 1;
unless (@ARGV && ((($ARGV[0] eq 'login' || $ARGV[0] eq 'clogin' || $ARGV[0] eq 'auth') && $numArgs == 3)  || ($ARGV[0] eq 'get_type' && $numArgs == 2) || ($ARGV[0] eq 'stdin' && $numArgs == 1))) { 
	print "Usage: $0 [clogin|login|auth] <username> <password>\n";
	print "Usage: $0 [get_type] <username>\n";
	print "Usage: $0 stdin\n";
	exit 1;
}

my $pass_hash1;
my $pass_hash2;
my $pass_hash3;
my @user_database;

# If the user database exists in the directory, load it into @user_database
if(-e "$Bin/userdata.udb"){
	my $user_ref = retrieve("$Bin/userdata.udb");
	@user_database = @{ dclone $user_ref };
}
# Else create a default user database with default user admin\pass admin.  Also creates control and monitor users that are disabled.
# It is assumed that an (A)dmin user can perform the same tasks as a (C)ontrol user and a (C)ontrol user can perform the same task as a (M)onitor user.
else{
	$pass_hash1 = sha256_hex("admin"."admin");
	$pass_hash2 = sha256_hex("control"."control");
	$pass_hash3 = sha256_hex("monitor"."monitor");

	@user_database = (
		{username => 'admin',  pname => 'admin', password  => $pass_hash1,  type  => 'A',  enabled => "true", expire => "NA"},
		{username => 'control', pname => 'control', password  => $pass_hash2,  type  => 'C',  enabled => "false", expire => "NA"},
		{username => 'monitor', pname => 'monitor', password  => $pass_hash3,  type  => 'M',  enabled => "false", expire => "NA"}
	);

}

my $action = $ARGV[0];
my $user = $ARGV[1];

if ($ARGV[0] eq "stdin") {
	print "Enter(clogin|login|auth|get_type): ";
	$action = <STDIN>;
	chomp($action);
	print "Enter user name: ";
	$user = <STDIN>;
	chomp($user);
}


# Check to see if user exists in database.

# Get the number of users in the database.
my $file_no = scalar (@user_database);
my $found = 0;
my $i=0;
my $usernum = -1;
my $enabled = 0;

# Search the database until the user is found.
while (($i < $file_no)&&(!$found))
{
	if ($user eq $user_database[$i]{'username'}){
		$usernum = $i;
		$found = 1;
		#check to see if guest user is expired
		if (($user_database[$i]{'type'} eq 'G') && ($user_database[$i]{'expire'} lt time())){
			# Deletes the user in $i position.
			splice(@user_database, $i, 1);
			if ($action eq 'login') { print "ADM: Guest user expired and deleted.\n"; }
			else { print RFILE "Guest user expired and deleted.\n"; }
			# Storing @user_database to a file.
			nstore \@user_database, "$Bin/userdata.udb";
			exit 13;
		}
		#check to see if account it enabled.
		if ($user_database[$i]{'enabled'} eq "true"){
			$enabled = 1;
		}
	}
	$i++;
}

# User was not found.
if (!$found){
	print "ADM: No such user.\n";
	exit 2;	
}

# User was found but is disabled.
if (!$enabled){
	print "ADM: User is disabled.\n";
	exit 3;	
}

# Get the user's user type.
my $type = $user_database[$usernum]{'type'};

# If the action is to verify user type:
if ($action eq "get_type"){
	if ($type eq 'A'){
		print "ADM: User is an Administrator\n";
		exit 4;
	} elsif ($type eq 'C'){
  		print "ADM: User is a Control user\n";
		exit 5;
	} elsif ($type eq 'M'){
  		print "ADM: User is a Monitor user\n";
		exit 6;
	} elsif ($type eq 'G'){
  		print "ADM: User is a Guest user\n";
		exit 11;
	}
}

# Authenticate the user.
if (($action eq 'login') || ($action eq 'clogin') || ($action eq 'auth')){
	my $password = $ARGV[2];
	if ($ARGV[0] eq "stdin") {
		print("Enter passwd: ");
		$password = <STDIN>;
		chomp($password);
	}

	my $passhash = sha256_hex($user_database[$usernum]{'pname'}.$password);
	my $userhash = $user_database[$usernum]{'password'};
	unless ($passhash eq $userhash){
		print "ADM: Bad password!\n";
		exit 7;
	}
}	

# If action was to only to authenticate the user:
if ($action eq "auth"){
	if ($type eq 'A'){
		print "ADM: User is an authenticated Administrator.\n";
		exit 8;
	} elsif ($type eq 'C'){
  		print "ADM: User is an authenticated Control user.\n";
		exit 9;
	} elsif ($type eq 'M'){
  		print "ADM: User is an authenticated Monitor user.\n";
		exit 10;
	} elsif ($type eq 'G'){
  		print "ADM: User is an authenticated Guest user.\n";
		exit 12;
	}
}

# Login to user admin system
if ($action eq 'login') { print "ADM: Welcome $user!\n"; }

# The user administration system performs the following functions for all users:
# - Help menu
# - Change the current user's password
#
# The user administration system performs the following functions for administrators only: 
# - add user
# - delete user
# - rename user
# - chage a user's password
# - disable a user
# - enable a user
# - set a user's type
# - show a list of users

my $task_string;
my @task_hash;
my $numTask;
my $task;
my $validtask;
if ($action eq 'clogin') {
	my $commands_file = "commands.txt";
	my $results_file = "results.txt";
	if ($ARGV[0] eq "stdin") {
		print "Enter comamnds file name: ";
		$commands_file = <STDIN>;
		chomp($commands_file);
		print "Enter results file name: ";
		$results_file = <STDIN>;
		chomp($results_file);
	}
	open CFILE, "$commands_file" or die $!;
	open (RFILE, ">$results_file");
}
 
do {
	$task = "null";
	$validtask = 0;
	if ($action eq 'login') { 
		print "diyalarm> ";
		$task_string = <STDIN>;
	}
	else {
		$task_string = <CFILE>;
	}
	@task_hash = split(' ',$task_string);
	$numTask = $#task_hash + 1;
	if ($numTask >  0){
		$task = $task_hash[0];
		# Checking to see if the requested task is valid:
		if (($task eq '?')||($task eq 'chgmypass')||($task eq 'exit')){
			$validtask = 1;
		# The tasks below are only valid if the user is an administrator.
		}elsif (($type eq 'A') && (($task eq 'adduser')||($task eq 'deluser')||($task eq 'rename')||($task eq 'chgpass'))){
			$validtask = 1;
		}elsif (($type eq 'A') && (($task eq 'disuser')||($task eq 'enuser')||($task eq 'settype')||($task eq 'showusers'))){ 
			$validtask = 1;
		}
	}
	# if a valid task was specified, go to the matching subroutine: 
	if ($validtask){
		if ($task eq '?') {
			&help;
		} elsif ($task eq 'chgmypass') {
			&mypass_change;
		} elsif ($task eq 'adduser') {
			&user_add;
		} elsif ($task eq 'deluser') {
			&user_delete;
		} elsif ($task eq 'rename') {
			&user_rename;
		} elsif ($task eq 'chgpass') {
			&user_chgpass;
		} elsif ($task eq 'disuser') {
			&user_disable;
		} elsif ($task eq 'enuser') {
			&user_enable;
		} elsif ($task eq 'settype') {
			&user_settype;
		} elsif ($task eq 'showusers') {
			&user_show;
		}
	} else {
		print "Type '?' for help or 'exit' to escape.\n";
	}
} while ($task ne 'exit');

if ($action eq 'clogin') {
	close (CFILE);
	close (RFILE); 
}

# Storing @user_database to a file.
nstore \@user_database, "$Bin/userdata.udb";

exit 0;

sub help {
	if ($type ne 'A') {
		print "Available Tasks:\n";
		print "\tChange my pass:		chgmypass old_pass new_pass new_pass\n"; 
	} else {
		print "Available Tasks:\n";
		print "\tChange my pass:		chgmypass old_pass new_pass new_pass\n";
		print "\tAdd a user:  		adduser username pass pass type [G exp.]\n";
		print "\tDelete a user:  	deluser username\n";
		print "\tRename a user:  	rename username new_username\n";
		print "\tChange user's pass:  	chgpass username new_pass new_pass\n";
		print "\tDisable a user:  	disuser username\n";
		print "\tEnable a user:  	enuser username\n";
		print "\tSet a user's type:  	settype username type\n";
		print "\tShow list of users: 	showusers\n";
	}
}

# This subroutine changes the current user's password.
sub mypass_change {
	if ($numTask == 4) {
		# Finds the user position in the database.
		if (find_user($user) != -1){
			$pass_hash1 = sha256_hex($user_database[$usernum]{'pname'}.$task_hash[1]);
			# Checking to see if old password entered matches the existing password.
			if ($pass_hash1 eq $user_database[$usernum]{'password'}){
				if ($task_hash[2] eq $task_hash[3]){
					$pass_hash2 = sha256_hex($user.$task_hash[2]);
					# Changing the password to the new password.
					$user_database[$usernum]{'password'} = $pass_hash2;
					# Changing the pname if different because of rename
					if ($user ne $user_database[$usernum]{'pname'}){
						$user_database[$usernum]{'pname'} = $user;
					}
					if ($action eq 'login') { print "ADM: Password changed.\n"; }
					else { print RFILE "Password changed.\n"; }
				} else {
					if ($action eq 'login') { print "ADM: New passwords do not match.\n"; }
					else { print RFILE "New passwords do not match.\n"; }	
				}
			} else {
				if ($action eq 'login') { print "ADM: Old password does not match.\n"; }
				else { print RFILE "Old password does not match.\n"; }
			}
		} else {
			# User just logged in so it should always exist. 
			print "ADM: User does not exists in database.\n";	
		}		
	} else {
		if ($action eq 'login') { print "ADM: Usage: chgmypass old_pass new_pass new_pass\n"; }
		else { print RFILE "Error: Passwords may not be blank or contain spaces.\n"; }
	}
}

# This subroutine adds a user to the database.
sub user_add {	
	if (($numTask == 5)||($numTask == 6 && $task_hash[4] eq 'G')) {
		if (find_user($task_hash[1]) == -1){
			if ($task_hash[2] eq $task_hash[3]){
				if (($task_hash[4] eq 'A')||($task_hash[4] eq 'C')||($task_hash[4] eq 'M')||($task_hash[4] eq 'G')){
					# Encrypt the user password.
					$pass_hash1 = sha256_hex($task_hash[1].$task_hash[2]);
					# Set user expiration date for guest users
					my $expire = 'NA';
					if ($task_hash[4] eq 'G') {
						#defualt expiration time in 60 minutes
						my $expire_time = 60;
						#check to see if a non-default experation time was entered
						if ($numTask == 6) {
							#check to see if experation time is numeric
							if ($task_hash[5] =~ /^[0-9]+$/) {
								$expire_time = $task_hash[5];
							}
							else {
								if ($action eq 'login') { print "ADM: Invalid expiration time.  Will be set to 60 min.\n"; }
								else { print RFILE "Invalid expiration time.  Will be set to 60 min.\n"; }
							}
						}
						#set guest expiration time to be current time plus minutes specified, converted to seconds
						$expire = time() + (60 * $expire_time);
					}
					$file_no = scalar (@user_database);
					$i = 0;
					# Search for where the username would fit in alphabetical order.
					while (($i < $file_no)&&($task_hash[1] gt $user_database[$i]{'username'})) {
						$i++;
					}
					# Insert the new user into the database.
					splice(@user_database, $i, 0, ({username => $task_hash[1], pname => $task_hash[1], password  => $pass_hash1,  type  => $task_hash[4],  enabled => "true", expire => $expire})); 
					if ($action eq 'login') { print "ADM: User successfully added.\n"; }
					else { print RFILE "User successfully added.\n"; }
				} else {
					print "ADM: Invalid user type.  Must be A, C, M, or G.\n";
				}
			} else {
				if ($action eq 'login') { print "ADM: Passwords do not match.\n"; }
				else { print RFILE "Passwords do not match.\n"; }	
			}
		} else {
			if ($action eq 'login') { print "ADM: User already exists in database.\n"; }
			else { print RFILE "User already exists in database.\n"; }	
		}
	} else {
		if ($action eq 'login') { print "ADM: Usage: adduser username pass pass type [exp. time for type G - default 60 min]\n"; }
		else { print RFILE "Error: Usernames and passwords cannot be blank or contain spaces.\n"; }
	}
}

# This subroutine deletes a user from the database.
sub user_delete {
	if ($numTask == 2) {
		if ($user eq $task_hash[1]){
			if ($action eq 'login') { print "ADM: Cannot delete own username.\n"; }
			else { print RFILE "Cannot delete own username.\n"; }
		} else {
			# Finds the user position in the database.
			if (find_user($task_hash[1]) != -1){
				if ($action eq 'login') { print "ADM: Are you sure you want to delete $task_hash[1]? Y to confirm: "; }
				my $answer = 'N';
				if ($action eq 'login') {
					chomp ($answer = <STDIN>);
				}
				else {
					chomp ($answer = <CFILE>);
				}	
				if (($answer eq "Y") || ($answer eq "y")){
					# Deletes the user in $usernum position.
					splice(@user_database, $usernum, 1);
					if ($action eq 'login') { print "ADM: User deleted.\n"; }
					else { print RFILE "User deleted.\n"; }
				}
				else { if ($action eq 'clogin') { print RFILE "User NOT deleted.\n"; } }
			} else {
				if ($action eq 'login') { print "ADM: User does not exists in database.\n"; }
				#The extra <CFILE> is to skip the unused $answer variable before exiting.
				else { print RFILE "User does not exists in database.\n"; <CFILE>; } 	
			}
		}		
	} else {
		if ($action eq 'login') { print "ADM: Usage: deluser username\n"; }
		#The extra <CFILE> is to skip the unused $answer variable before exiting.
		else { print RFILE "Error: Username cannot be blank or contain spaces.\n"; <CFILE>; }
	}
}

# This subroutine renames a user.
sub user_rename {
	if ($numTask == 3) {
		# Check to see if the user to rename exists.
		my $user_pos = find_user($task_hash[1]);
		if ($user_pos != -1){
			# Check to see if new name already exists.
			if (find_user($task_hash[2]) == -1){
				# Make a copy of the user to rename.
			        my $user_hash_pname = $user_database[$user_pos]{'pname'};
				my $user_hash_pass = $user_database[$user_pos]{'password'};
				my $user_hash_type = $user_database[$user_pos]{'type'};
				my $user_hash_enabled = $user_database[$user_pos]{'enabled'};
				my $user_hash_expire = $user_database[$user_pos]{'expire'};
				# Add a new user in alphabetical order to the database.
				$file_no = scalar (@user_database);
				$i = 0;
				# Search for where the username would fit in alphabetical order.
				while (($i < $file_no)&&($task_hash[2] gt $user_database[$i]{'username'})) {
					$i++;
				}
				# Insert the new name into the database.
				splice(@user_database, $i, 0, ({username => $task_hash[2],  pname => $user_hash_pname, password  => $user_hash_pass,  type  => $user_hash_type,  enabled => $user_hash_enabled, expire => $user_hash_expire})); 
				# Remove the old name from the database.
				# Find the new position of the old name after the new name was entered.
				$user_pos = find_user($task_hash[1]);
				# Remove the user at $user_pos
				splice(@user_database, $user_pos, 1);
				# If the user logged in was renamed then update the logged-in user variable
				if ($user eq $task_hash[1]) {
					$user = $task_hash[2];
				}
				if ($action eq 'login') { print "ADM: Name changed.\n"; }
				else { print RFILE "Name changed.\n"; }
			} else {
				if ($action eq 'login') { print "ADM: New name already exists in the database.\n"; }
				else { print RFILE "New name already exists in the database.\n"; }	
			}
		} else {
			if ($action eq 'login') { print "ADM: User does not exists in database.\n"; }
			else { print RFILE "User does not exists in database.\n"; }
		}
	} else {
		if ($action eq 'login') { print "ADM: Usage: rename username new_username\n"; }
		else { print RFILE "Error: Username cannot be blank or contain spaces.\n"; }
	}
}

# This subroutine changes a user's password. 
sub user_chgpass {
	if ($numTask == 4) {
		# Finds the user position in the database.
		if (find_user($task_hash[1]) != -1){
			if ($task_hash[2] eq $task_hash[3]){
				$pass_hash1 = sha256_hex($task_hash[1].$task_hash[2]);
				$user_database[$usernum]{'password'} = $pass_hash1;
				# Changing the pname if different because of rename
				if ($task_hash[1] ne $user_database[$usernum]{'pname'}){
					$user_database[$usernum]{'pname'} = $task_hash[1];
				}
				if ($action eq 'login') { print "ADM: Password changed.\n"; }
				else { print RFILE "Password changed.\n"; }
			} else {
				if ($action eq 'login') { print "ADM: Passwords do not match.\n"; }
				else { print RFILE "Passwords do not match.\n"; }	
			}
		} else {
			if ($action eq 'login') { print "ADM: User does not exists in database.\n"; }	
			else { print RFILE "User does not exists in database.\n"; }
		}		
	} else {
		if ($action eq 'login') { print "ADM: Usage: chgpass username new_pass new_pass\n"; }
		else { print RFILE "Error: Usernames and passwords cannot be blank or contain spaces.\n"; }
	}
}

# This subroutine disables a user.
sub user_disable {
	if ($numTask == 2) {
		if ($user eq $task_hash[1]){
			if ($action eq 'login') { print "ADM: Cannot disable own username.\n"; }
			else { print RFILE "Cannot disable own username.\n"; }
		} else {
			# Finds the user position in the database.
			if (find_user($task_hash[1]) != -1){
				$user_database[$usernum]{'enabled'} = "false";
				if ($action eq 'login') { print "ADM: User disabled.\n"; }
				else { print RFILE "User disabled.\n"; }
			} else {
				if ($action eq 'login') { print "ADM: User does not exists in database.\n"; }
				else { print RFILE "User does not exists in database.\n"; }	
			}
		}		
	} else {
		if ($action eq 'login') { print "ADM: Usage: disuser username\n"; }
		else { print RFILE "Error: Username cannot be blank or contain spaces.\n"; }
	}
}

# This subroutine enable a user.
sub user_enable {
	if ($numTask == 2) {
		# Finds the user position in the database.
		if (find_user($task_hash[1]) != -1){
			$user_database[$usernum]{'enabled'} = "true";
			if ($action eq 'login') { print "ADM: User enabled.\n"; }
			else { print RFILE "User enabled.\n"; }
		} else {
			if ($action eq 'login') { print "ADM: User does not exists in database.\n"; }
			else { print RFILE "User does not exists in database.\n"; }	
		}		
	} else {
		if ($action eq 'login') { print "ADM: Usage: enuser username\n"; }
		else { print RFILE "Error: Username cannot be blank or contain spaces.\n"; }
	}
}

# This subroutine set a user's type.
sub user_settype {
	if ($numTask == 3) {
		if ($user eq $task_hash[1]){
			if ($action eq 'login') { print "ADM: Cannot change type on own username.\n"; }
			else { print RFILE "Cannot change type on own username.\n"; }
		} else {
			# Finds the user position in the database.
			if (find_user($task_hash[1]) != -1){				
				if (($task_hash[2] eq 'A')||($task_hash[2] eq 'C')||($task_hash[2] eq 'M')||($task_hash[2] eq 'G')){
					$user_database[$usernum]{'type'} = $task_hash[2];
					if ($action eq 'login') { print "ADM: User type set.\n"; }
					else { print RFILE "User type set.\n"; }
					if ($task_hash[2] eq 'G') {
						my $expire = time() + (60 * 60);
						$user_database[$usernum]{'expire'} = $expire;
						if ($action eq 'login') { print "ADM: New guest account will expire in 60 minutes.\n"; }
						else { print RFILE "New guest account will expire in 60 minutes.\n"; }
					}
					else {
						if ($user_database[$usernum]{'expire'} ne 'NA') {
							$user_database[$usernum]{'expire'} = 'NA';
						}
					}
				} else {
					print "ADM: Invalid user type.  Must be A, C, M, or G.\n";
				}
			} else {
				if ($action eq 'login') { print "ADM: User does not exists in database.\n"; }
				else { print RFILE "User does not exists in database.\n"; }	
			}
		}		
	} else {
		if ($action eq 'login') { print "ADM: Usage: settype username type\n"; }
		else { print RFILE "Error: Username cannot be blank or contain spaces.\n"; }
	}
}

# This subroutine prints out the users in the database.
sub user_show {
	# Get the number of users in the database
	$file_no = scalar (@user_database);
	if ($action eq 'login') { print "ADM: User Database:\n\n"; }
	else { print RFILE "User Database:\n\n"; }
	# Looping through the hash and printing out all the hash "file" elements.
	for ($i=0; $i < $file_no; $i++)
	{
		my $expire = 'NA';
		#check to see if guest user is expired 
		if (($user_database[$i]{'type'} eq 'G')&&($user_database[$i]{'expire'} lt time())){
			# Deletes the user in $i position.
			splice(@user_database, $i, 1);
			$file_no = $file_no-1;
			$i = $i-1;
		}
		else {
			if ($user_database[$i]{'type'} eq 'G'){
				# Change expire time to a readable format.
				$expire = localtime($user_database[$i]{'expire'});
			}
			if ($action eq 'login') { print "ADM: User = $user_database[$i]{'username'}\tType = $user_database[$i]{'type'}\tEnabled = $user_database[$i]{'enabled'}\tExpiration Date = $expire\n"; }
			else { print RFILE "User = $user_database[$i]{'username'}\tType = $user_database[$i]{'type'}\tEnabled = $user_database[$i]{'enabled'}\tExpiration Date = $expire\n"; }
		}
	}
	if ($action eq 'login') { print "\nADM: Total users = $file_no\n"; }
	else { print RFILE "\nTotal users = $file_no\n"; }
}

#This subroutine finds a user in the database and returns its location.  
sub find_user {
	# Get the number of users in the database.
	$file_no = scalar (@user_database);
	$found = 0;
	$i=0;
	$usernum = -1;

	# Search the database until the user is found.
	while (($i < $file_no)&&(!$found))
	{
		if ($_[0] eq $user_database[$i]{'username'}){
			$usernum = $i;
			$found = 1;
		}
		$i++;
	}
	return $usernum;	
}

# EOF
